/*
 * $Id: QEADIFFileMaker.m 99 2009-07-26 19:30:06Z jon@bway.net $
 * Chalk
 *
 * Created 6/7/09
 * Copyright Jon Gordon
 * 
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to 
 * permit persons to whom the Software is furnished to do so, subject to 
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#import "QEADIFFileMaker.h"

#include <zlib.h>

#import "QEADIFInternal.h"

#import "QEADIFDataField.h"
#import "QEADIFElement.h"
#import "QEADIFFile.h"
#import "QEADIFParser.h"
#import "QEADIFValue.h"

#import "QEBand.h"

#define PROGRAMID_FOR_LOTW @"LoTW"

#define CUSTOM_FIELD_NAME_REGEX @"\\AAPP_[_a-zA-Z]+\\z"

#define TAG_END_OF_HEADER @"EOH"
#define TAG_END_OF_RECORD @"EOR"

#define GUNZIP_BUFFER_SIZE ((NSUInteger) 4096)


@interface QEADIFFileMaker (private)
- (id)initWithElements:(NSArray *)allElements;

- (void)nextElement;

- (void)handlePossibleHeader;
- (void)handleBodyWithoutHeader;
- (void)handleBody;
- (void)handleEOIInHeader;
- (void)handleEOI;
- (void)handleError;
- (void)handleField;
- (void)handleTagInHeader;
- (void)handleTag;
- (void)convertHeaderToRecord;
- (void)handleFakeHeader;
- (void)handleIllegalHeaderField:(QEADIFElement *)srcElement;
- (void)handleBadValueWithError:(NSError *)error;
- (void)addHeader;
- (void)addRecord;
- (void)detectNeededCleanup;
- (void)cleanUpRecord;

@end

static NSSortDescriptor *elementLineDescriptor;
static NSSortDescriptor *elementPositionDescriptor;
static NSArray *elementSortDescriptors;

static NSPredicate *customFieldNamePred;

@implementation QEADIFFileMaker

@synthesize elements;


+ (QEADIFFile *)makeADIFFileFromSource:(NSString *)source
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (nil == source) {
        return nil;
    } // if
    
    NSArray *elements = [QEADIFParser cleanedElementsFromSource:source];
    NSAssert(nil != elements,
             @"Non-nil source should produce non-nil list of elements.");
    
    QEADIFFileMaker *maker = [[QEADIFFileMaker alloc] initWithElements:elements];
    QEADIFFile *result = [maker makeFile];
    [maker release];
    return result;
}  // makeFileFromSource:


+ (QEADIFFile *)makeADIFFileFromFile:(NSString *)filename error:(NSError **)outError
{
    QEADIFFile *result = nil;
    NSStringEncoding enc;
    NSError *error = nil;
    
    NSString *srcText = [NSString stringWithContentsOfFile:filename
                                              usedEncoding:&enc
                                                     error:&error];
    
    if (nil != srcText) {
        result = [QEADIFFileMaker makeADIFFileFromSource:srcText];
    } // if
    
    NSFileManager *fm = [NSFileManager defaultManager];
    if (nil != result) {
        [result setFilename:filename];
        
        NSDictionary *attributes = [fm fileAttributesAtPath:filename traverseLink:YES];
        [result setCreateDate:[attributes objectForKey:NSFileCreationDate]];
        [result setModifyDate:[attributes objectForKey:NSFileModificationDate]];
    } // if
    
    if ((nil != error) && (nil != outError)) {
        *outError = error;
    } //if
    
    return result;
} // makeADIFFileFromFile:


+ (NSDictionary *)makeADIFRecordFromSource:(NSString *)source
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (nil == source) return nil;
    
    NSArray *elements = [QEADIFParser allElementsFromSource:source];
    NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:[elements count]];
    
    for (QEADIFElement *element in elements) {
        if ([element isEOI]) break;
        
        NSString *fieldName = [element fieldName];        
        NSString *fieldValueSrc = [element fieldValue];
        unichar dataTypeCode;
        QEADIFType dataType;
        
        if ([customFieldNamePred evaluateWithObject:fieldName]) {
            dataType = qeADIFTypeUndefined;
            dataTypeCode = qeADIFDataTypeUnspecified;
        } else {
            dataType = [QEADIFValue typeForFieldName:fieldName];
        } // else
        
        if (qeADIFTypeUndefined == dataType) {
            dataTypeCode = [element typeCode];
            dataType = [QEADIFValue adifTypeCode2Type:dataTypeCode];
        } // if
        
        id enumerationSpec = nil;
        if (qeADIFTypeEnumeration == dataType) {
            enumerationSpec = [QEADIFValue enumerationSpecForFieldName:fieldName];
            NSAssert1(nil != enumerationSpec,
                      @"  enumeration specifier for field name \"%@\" is nil",
                      fieldName);
        } // if
        
        QEADIFValue *fieldValue = nil;
        switch (dataType) {
            case qeADIFTypeAwardList:
                if ([QEADIFValue isValidAwardListSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue awardListFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeBoolean:
                if ([QEADIFValue isValidBooleanSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue booleanFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeDigit:
                if ([QEADIFValue isValidDigitSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue digitFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeNumber:
                if ([QEADIFValue isValidNumberSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue numberFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeDate:
                if ([QEADIFValue isValidDateSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue dateFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeTime:
                if ([QEADIFValue isValidTimeSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue timeFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeCharacter:
                if ([QEADIFValue isValidCharacterSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue characterFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeString:
                if ([QEADIFValue isValidStringSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue stringFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeUndefined:   // treat it as a string & hope for the best
                if ([QEADIFValue isValidStringSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue stringFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeMultilineString:
                if ([QEADIFValue isValidMultilineStringSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue multilineStringFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeLocation:
                if ([QEADIFValue isValidLocationSrc:&fieldValueSrc error:nil]) {
                    fieldValue = [QEADIFValue locationFromString:fieldValueSrc];
                } // if
                break;
            case qeADIFTypeEnumeration:
                if ([QEADIFValue isValidValueSrc:&fieldValueSrc
                                 fromEnumeration:enumerationSpec error:nil]) {
                    fieldValue = [QEADIFValue valueFromString:fieldValueSrc
                                              fromEnumeration:enumerationSpec];
                } // if
                break;
            default:    // CAN'T HAPPEN
                NSAssert1(NO,
                          @"  CAN'T HAPPEN: presented with undefined data type number %ld",
                          ((long) dataType));
                break;
        } // switch
        
        [result setObject:[[[QEADIFDataField alloc] initWithFieldName:fieldName
                                                             typeCode:dataTypeCode
                                                           fieldValue:fieldValue] autorelease]
                   forKey:fieldName];
    } // for
    
    return result;
} // makeADIFRecordFromSource:


#pragma mark LIFECYCLE METHODS
+ (BOOL)accessInstanceVariablesDirectly
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return NO;
} // accessInstanceVariablesDirectly


+ (void)initialize
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    static BOOL initialized = NO;
    
    if (!initialized) {
        //-NSLog(@"  initializing");
        initialized = YES;
        
        elementLineDescriptor = [[NSSortDescriptor alloc] initWithKey:@"line"
                                                            ascending:YES];
        elementPositionDescriptor = [[NSSortDescriptor alloc] initWithKey:@"position"
                                                                ascending:YES];
        elementSortDescriptors = [[NSArray alloc] initWithObjects:elementLineDescriptor,
                                  elementPositionDescriptor,
                                  nil];

        customFieldNamePred = [[NSPredicate predicateWithFormat:@"SELF matches %@",
                                CUSTOM_FIELD_NAME_REGEX] retain];
    } // if        
} // initialize


- (id)init
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return [self initWithElements:nil];
} // init

- (id)initWithElements:(NSArray *)allElements
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    self = [super init];
    if (nil != self) {
        tmpHeaders = nil;
        tmpRecords = nil;
        tmpErrors = nil;
        tmpRecord = [[NSMutableDictionary alloc] init];
        [self setElements:allElements];
    } // if
    
    return self;
} // initWithElements:

- (void)dealloc
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [self setElements:nil];
    [tmpRecord release];
    [super dealloc];
} // dealloc


#pragma mark OPERATIONS
- (QEADIFFile *)makeFile
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (nil == elements) {
        //-NSLog(@"  WARNING: ADIF file maker asked to make file from nil list of ADIF elements.");
        return [[[QEADIFFile alloc] init] autorelease];
    } // if
    
    ee = [elements objectEnumerator];
    element = [ee nextObject];
    NSAssert(nil != element,
             @"Parser is not supposed to create zero-length element list.");
    
    QEADIFFile *result = [[QEADIFFile alloc] init];
    tmpHeaders = [[NSMutableArray alloc] init];
    tmpRecords = [[NSMutableArray alloc] init];
    tmpErrors = [[NSMutableArray alloc] init];
    
    hasHeader = NO;
    inHeader = NO;
    isLoTW = NO;
    
    while ([element elementType] == qeADIFElementError) {
        [tmpErrors addObject:element];
        [self nextElement];
    } // while
    
    switch ([element elementType]) {
        case qeADIFElementComment:
            [self handlePossibleHeader];
            [self handleBody];
            break;
        case qeADIFElementEOI:
            break;
        default:
            [self handleBody];
            break;
    } // switch
    
    [result setAdifHeaders:tmpHeaders];
    [result setAdifRecords:tmpRecords];
    [result setAdifErrors:[tmpErrors sortedArrayUsingDescriptors:elementSortDescriptors]];
    
    [tmpHeaders release];
    [tmpRecords release];
    [tmpErrors release];
    return [result autorelease];
} // makeFile


# pragma mark INPUT PROCESSING
- (void)nextElement
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if (![element isEOI]) {
        element = [ee nextObject];
        NSAssert(nil != element,
                 @"  ERROR: Element stream is not terminated with EOI.");
    } // if
} // nextElement

- (void)handlePossibleHeader
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    
    headerOnlyFieldsSeen = [[NSMutableSet alloc] init];
    hasHeader = YES;
    inHeader = YES;
    
    // We know going in that the current element is the first element that
    // the parser found in the data and that it's a comment.  We also know
    // that the program that created the data is *supposed* to end the header
    // with an <EOH> tag, and that the parser is *supposed* to end the data
    // with an EOI element.
    while (inHeader) {
        switch ([element elementType]) {
            case qeADIFElementEOI:
                [self handleEOIInHeader];
                // DO NOT ADVANCE ELEMENT
                break;
            case qeADIFElementError:
                [self handleError];
                [self nextElement];
                break;
            case qeADIFElementField:
                [self handleField];
                [self nextElement];
                break;
            case qeADIFElementTag:
                [self handleTagInHeader];
                [self nextElement];
                break;
            default:
                [self nextElement];
                // assume anything else is a comment--which it should be--
                // and ignore it
        } // switch
    } // while
    
    [headerOnlyFieldsSeen release];
} // handlePossibleHeader

- (void)handleBodyWithoutHeader
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [self handleBody];
} // handleBodyWithoutHeader

- (void)handleBody
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    while (![element isEOI]) {
        switch ([element elementType]) {
            case qeADIFElementEOI:
                [self handleEOI];
                // DO NOT ADVANCE ELEMENT
                break;
            case qeADIFElementError:
                [self handleError];
                [self nextElement];
                break;
            case qeADIFElementField:
                [self handleField];
                [self nextElement];
                break;
            case qeADIFElementTag:
                [self handleTag];
                [self nextElement];
                break;
            default:
                [self nextElement];
                // assume anything else is a comment--which it should be--
                // and ignore it
        } // switch
    } // while
    //-NSLog(@"  exiting main loop of handleBody");
} // handleBody

- (void)handleEOIInHeader
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [self addHeader];
} // handleEOIInHeader

- (void)handleEOI
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [self addRecord];
} // handleEOI

- (void)handleError
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [tmpErrors addObject:element];
} // handleError

- (void)handleField
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSString *fieldName = [element fieldName];
    
    if ([QEADIFValue isHeaderOnlyField:fieldName]) {
        if (inHeader) {
            [headerOnlyFieldsSeen addObject:element];
        } else {
            [self handleIllegalHeaderField:element];
            return;
        } // else
    } // if
    
    NSString *fieldValueSrc = [element fieldValue];
    unichar dataTypeCode;
    QEADIFType dataType;
    if ([customFieldNamePred evaluateWithObject:fieldName]) {
        //-NSLog(@"  custom field name \"%@\" encountered", fieldName);
        dataType = qeADIFTypeUndefined;
        dataTypeCode = qeADIFDataTypeUnspecified;
    } else {
        dataType = [QEADIFValue typeForFieldName:fieldName];
        if (qeADIFTypeUndefined == dataType) {
            //-NSLog(@"  field name \"%@\" appears to be custom field, but doesn't begin with \"APP_\"", \
                  fieldName);
        } // if
    } // else
    
    if (qeADIFTypeUndefined == dataType) {
        //-NSLog(@"  data type not inherent in type name, looking for specifier in ADIF element");
        dataTypeCode = [element typeCode];
        dataType = [QEADIFValue adifTypeCode2Type:dataTypeCode];
    } // if
    
    id enumerationSpec = nil;
    if (qeADIFTypeEnumeration == dataType) {
        enumerationSpec = [QEADIFValue enumerationSpecForFieldName:fieldName];
        NSAssert1(nil != enumerationSpec,
                  @"  enumeration specifier for field name \"%@\" is nil",
                  fieldName);
    } // if
    
    QEADIFValue *fieldValue = nil;
    NSError *error = nil;
    switch (dataType) {
        case qeADIFTypeAwardList:
            if ([QEADIFValue isValidAwardListSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue awardListFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeBoolean:
            if ([QEADIFValue isValidBooleanSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue booleanFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeDigit:
            if ([QEADIFValue isValidDigitSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue digitFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeNumber:
            if ([QEADIFValue isValidNumberSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue numberFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeDate:
            if ([QEADIFValue isValidDateSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue dateFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeTime:
            if ([QEADIFValue isValidTimeSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue timeFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeCharacter:
            if ([QEADIFValue isValidCharacterSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue characterFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeString:
            if ([QEADIFValue isValidStringSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue stringFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeUndefined:   // treat it as a string & hope for the best
            if ([QEADIFValue isValidStringSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue stringFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeMultilineString:
            if ([QEADIFValue isValidMultilineStringSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue multilineStringFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeLocation:
            if ([QEADIFValue isValidLocationSrc:&fieldValueSrc error:&error]) {
                fieldValue = [QEADIFValue locationFromString:fieldValueSrc];
            } // if
            break;
        case qeADIFTypeEnumeration:
            if ([QEADIFValue isValidValueSrc:&fieldValueSrc
                             fromEnumeration:enumerationSpec error:&error]) {
                fieldValue = [QEADIFValue valueFromString:fieldValueSrc
                                          fromEnumeration:enumerationSpec];
            } // if
            break;
        default:    // CAN'T HAPPEN
            NSAssert1(NO,
                      @"  CAN'T HAPPEN: presented with undefined data type number %ld",
                      ((long) dataType));
            break;
    } // switch
    
    if (nil != error) {
        [self handleBadValueWithError:error];
    } else {
        [tmpRecord setObject:[[[QEADIFDataField alloc] initWithFieldName:fieldName
                                                                typeCode:dataTypeCode
                                                              fieldValue:fieldValue] autorelease]
                      forKey:fieldName];
    } // else
} // handleField

- (void)handleTagInHeader
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if ([TAG_END_OF_RECORD isEqual:[element fieldName]]) {
        // <eor> isn't supposed to appear in header; if it does, assume "header" was
        // supposed to be a QSO record, not a header.
        [self handleFakeHeader];
    } else if ([TAG_END_OF_HEADER isEqual:[element fieldName]]) {
        [self addHeader];
    } else {
        //-NSLog(@"  INFO: unsupported tag \"<%@>\" encountered in input at line %lu, position %lu", \
              [element fieldName], \
              ((unsigned long) [element line]), \
              ((unsigned long) [element position]));
    } // else
} // handleTagInHeader

- (void)handleTag
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    if ([TAG_END_OF_RECORD isEqual:[element fieldName]]) {
        [self addRecord];
    } else {
        //-NSLog(@"  INFO: unsupported tag \"<%@>\" encountered in input at line %lu, position %lu", \
              [element fieldName], \
              ((unsigned long) [element line]), \
              ((unsigned long) [element position]));
    } // else
} // handleTag

- (void)convertHeaderToRecord
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    
    // We thought that we were processing elements for an ADIF header.
    // In a wrenching twist, however, we unexpectedly encountered an
    // <eor> tag instead of a data field, a comment, or an <eoh> tag.
    // Rather than choke and die on this input, however, we're going
    // to treat the fields that we've seen so far as fields in a record
    // instead of fields in a header.
    //
    // Some fields are valid only in a header, however.  So now, if any
    // header-only fields were encountered, we have to flag the errors.
    
    for (QEADIFElement *e in headerOnlyFieldsSeen) {
        [self handleIllegalHeaderField:e];
    } // for
} // convertHeaderToRecord

- (void)handleIllegalHeaderField:(QEADIFElement *)srcElement
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    
    NSString *format = NSLocalizedString(@"ADIF format error at line %lu, position %lu: field \"%@\" allowed only in ADIF header.",
                                         @"");
    NSString *eMsg = [NSString stringWithFormat:format, 
                      ((unsigned long) [srcElement line]), 
                      ((unsigned long) [srcElement position]),
                      [srcElement fieldName]];
    NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
    NSError *newError = [NSError errorWithDomain:QEADIFErrorDomain
                                            code:QEADIFErrorHeaderFieldInBody
                                        userInfo:eDict];
    [tmpErrors addObject:[QEADIFElement elementError:newError
                                          fromSource:[srcElement src] 
                                              atLine:[srcElement line]
                                         andPosition:[srcElement position]]];
} // handleIllegalHeaderElement:

- (void)handleFakeHeader
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    hasHeader = NO;
    inHeader = NO;
    [self convertHeaderToRecord];
    [self addRecord];
} // handleFakeHeader

- (void)addHeader
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    
    [self detectNeededCleanup];
    [tmpHeaders setArray:[tmpRecord allValues]];
    [tmpRecord removeAllObjects];
    inHeader = NO;
} // addHeader

- (void)addRecord
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [self cleanUpRecord];
    if (0 < [tmpRecord count]) {
        [tmpRecords addObject:[tmpRecord allValues]];
        [tmpRecord removeAllObjects];
    } // if
} // addRecord

- (void)handleBadValueWithError:(NSError *)error
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    
    NSString *format = NSLocalizedString(@"Data validation error at line %lu, position %lu: %@",
                                         @"");
    NSString *eMsg = [NSString stringWithFormat:format, 
                      ((unsigned long) [element line]), 
                      ((unsigned long) [element position]),
                      [[error userInfo] objectForKey:NSLocalizedDescriptionKey]];
    NSDictionary *eDict = [NSDictionary dictionaryWithObjectsAndKeys:eMsg, NSLocalizedDescriptionKey, nil];
    NSError *newError = [NSError errorWithDomain:QEADIFErrorDomain
                                            code:[error code]
                                        userInfo:eDict];
    [tmpErrors addObject:[QEADIFElement elementError:newError
                                          fromSource:[element src] 
                                              atLine:[element line]
                                         andPosition:[element position]]];
} // recordBadValue:forField:


// Some programs produce non-standard ADIF output.  For example, the ADIF
// 2.2.3 standard provides several data fields for QSL information, but
// also provides several other fields for for QSL information that's specific
// to a particular source, such as ARRL's Logbook of the World (LoTW).  But if
// you get your ADIF file by exporting data from LoTW, it doesn't use the
// LoTW tags; it uses the general ones instead.
//
// This method uses information in the header to try to identify the source
// of the ADIF data and any consequent cleanup that may be needed.  Note that
// it gets its information by examining tmpRecord, not tmpHeader, so it needs
// to be called before the header information has been cleared from tmpRecord.
- (void)detectNeededCleanup
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    
    // currently, only LoTW is detected or repaired
    isLoTW = [PROGRAMID_FOR_LOTW isEqual:[[((QEADIFDataField *) [tmpRecord objectForKey:@"PROGRAMID"]) fieldValue] stringValue]];
    if (isLoTW) {
        //-NSLog(@"  LoTW cleanup needed");
    } else {
        //-NSLog(@"  no LoTW cleanup");
    } // else
} // detectNeededCleanup


- (void)cleanUpRecord
{
    //-NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    //-NSLog(@"  original record:\n%@\n", tmpRecord);
    QEADIFDataField *oldField;
    QEADIFDataField *newField;
    if (isLoTW) {
        //-NSLog(@"  cleaning up LoTW strangeness");
        oldField = [tmpRecord objectForKey:@"QSLRDATE"];
        if ((nil != oldField) && (nil == [tmpRecord objectForKey:@"LOTW_QSLRDATE"])) {
            newField = [[QEADIFDataField alloc] initWithFieldName:@"LOTW_QSLRDATE"
                                                         typeCode:[oldField typeCode]
                                                       fieldValue:[oldField fieldValue]];
            [tmpRecord setObject:newField forKey:@"LOTW_QSLRDATE"];
            [tmpRecord removeObjectForKey:@"QSLRDATE"];
            [newField release];
        } // if
        
        oldField = [tmpRecord objectForKey:@"QSLSDATE"];
        if ((nil != oldField) && (nil == [tmpRecord objectForKey:@"LOTW_QSLSDATE"])) {
            newField = [[QEADIFDataField alloc] initWithFieldName:@"LOTW_QSLSDATE"
                                                         typeCode:[oldField typeCode]
                                                       fieldValue:[oldField fieldValue]];
            [tmpRecord setObject:newField forKey:@"LOTW_QSLSDATE"];
            [tmpRecord removeObjectForKey:@"QSLSDATE"];
            [newField release];
        } // if
        
        oldField = [tmpRecord objectForKey:@"QSL_RCVD"];
        if ((nil != oldField) && (nil == [tmpRecord objectForKey:@"LOTW_QSL_RCVD"])) {
            newField = [[QEADIFDataField alloc] initWithFieldName:@"LOTW_QSL_RCVD"
                                                         typeCode:[oldField typeCode]
                                                       fieldValue:[oldField fieldValue]];
            [tmpRecord setObject:newField forKey:@"LOTW_QSL_RCVD"];
            [tmpRecord removeObjectForKey:@"QSL_RCVD"];
            [newField release];
        } // if
        // LoTW doesn't include a field indicating that you sent a QSL, even though it
        // won't tell you about received QSLs except for those that match ones that
        // you've sent.  Add it so importing can work.
        if (nil != oldField) {
            //-NSLog(@"  [[oldField fieldValue] stringValue]: %@", [[oldField fieldValue] stringValue]);
            //-NSLog(@"  [tmpRecord objectForKey:@\"QSL_SENT\"]: %@", [tmpRecord objectForKey:@"QSL_SENT"]);
            //-NSLog(@"  [tmpRecord objectForKey:@\"LOTW_QSL_SENT\"]: %@", [tmpRecord objectForKey:@"LOTW_QSL_SENT"]);
            if ([@"Y" isEqualToString:[[oldField fieldValue] stringValue]] &&
                (nil == [tmpRecord objectForKey:@"QSL_SENT"]) &&
                (nil == [tmpRecord objectForKey:@"LOTW_QSL_SENT"])) {
                //-NSLog(@"  supplying QSL sent field");
                id enumerationSpec = [QEADIFValue enumerationSpecForFieldName:@"LOTW_QSL_SENT"];
                QEADIFValue *newValue = [QEADIFValue valueFromString:@"Y" fromEnumeration:enumerationSpec];
                newField = [[QEADIFDataField alloc] initWithFieldName:@"LOTW_QSL_SENT"
                                                             typeCode:[oldField typeCode]
                                                           fieldValue:newValue];
                [tmpRecord setObject:newField forKey:@"LOTW_QSL_SENT"];
                [newField release];
            } // if
        } // if
        
        oldField = [tmpRecord objectForKey:@"QSL_SENT"];
        if ((nil != oldField) && (nil == [tmpRecord objectForKey:@"LOTW_QSL_SENT"])) {
            newField = [[QEADIFDataField alloc] initWithFieldName:@"LOTW_QSL_SENT"
                                                         typeCode:[oldField typeCode]
                                                       fieldValue:[oldField fieldValue]];
            [tmpRecord setObject:newField forKey:@"LOTW_QSL_SENT"];
            [tmpRecord removeObjectForKey:@"QSL_SENT"];
            [newField release];
        } // if
    } // if
    
    // ADIF frequencies are supposed to be in MHz.  ADIF frequencies are often
    // in kHz.  This is a problem.  To fix it, we compare the frequency against
    // the value in the BAND field.  If (1) BAND is set, (2) the frequency is
    // NOT within the legal limits for the BAND, and (3) frequency/1000 IS
    // within the legal band limits, we divide the frequency by 1000.
    QEADIFDataField *bandField = [tmpRecord objectForKey:@"BAND"];
    QEADIFDataField *freqField = [tmpRecord objectForKey:@"FREQ"];
    if ((nil != bandField) && (nil != freqField)) {
        NSString *bandName = [[bandField fieldValue] stringValue];
        if ([QEBand isValidBandName:&bandName]) {
            QEBand *band = [QEBand bandForName:bandName];
            NSDecimalNumber *freq = [[freqField fieldValue] decimalValue];
            if (![band containsFrequency:freq]) {
                freq = [freq decimalNumberByMultiplyingByPowerOf10:(-3)];
                if ([band containsFrequency:freq]) {
                    freqField = [[[QEADIFDataField alloc] 
                                  initWithFieldName:@"FREQ"
                                  fieldValue:[QEADIFValue numberFromDecimalNumber:freq]] autorelease];
                    [tmpRecord setObject:freqField forKey:@"FREQ"];
                } // if
            } // if
        } // if
    } // if
    
    //-NSLog(@"  cleaned-up record:\n%@\n", tmpRecord);
} // cleanUpRecord
@end
